Add "--list" option to `cargo`, that shows lists of installed (sub)commands by searching
authorxanxys <xanxys@gmail.com>
Sat, 9 Aug 2014 05:37:50 +0000 (14:37 +0900)
committerxanxys <xanxys@gmail.com>
Sat, 9 Aug 2014 05:37:50 +0000 (14:37 +0900)
directories for executables with name cargo-*.

src/bin/cargo.rs
tests/test_cargo.rs [new file with mode: 0644]
tests/tests.rs

index 828cfd91c8e4731505a2cf96fab40fdb011576f1..ac2a3e06f604ea38645447b2cb1d3a3c41eaa340 100644 (file)
@@ -7,7 +7,10 @@ extern crate cargo;
 extern crate docopt;
 #[phase(plugin)] extern crate docopt_macros;
 
+use std::collections::TreeSet;
 use std::os;
+use std::io;
+use std::io::fs;
 use std::io::process::{Command,InheritFd,ExitStatus,ExitSignal};
 use serialize::Encodable;
 use docopt::FlagParser;
@@ -28,10 +31,12 @@ Usage:
     cargo <command> [<args>...]
     cargo -h | --help
     cargo -V | --version
+    cargo --list
 
 Options:
     -h, --help       Display this message
     -V, --version    Print version info and exit
+    --list           List installed commands
     -v, --verbose    Use verbose output
 
 Some common cargo commands are:
@@ -54,6 +59,14 @@ See 'cargo help <command>' for more information on a specific command.
 fn execute(flags: Flags, shell: &mut MultiShell) -> CliResult<Option<()>> {
     debug!("executing; cmd=cargo; args={}", os::args());
     shell.set_verbose(flags.flag_verbose);
+    if flags.flag_list {
+        println!("Installed Commands:");
+        for command in list_commands().iter() {
+            println!("    {}", command);
+            // TODO: it might be helpful to add result of -h to each command.
+        };
+        return Ok(None)
+    }
     let mut args = flags.arg_args.clone();
     args.insert(0, flags.arg_command.clone());
     match flags.arg_command.as_slice() {
@@ -82,29 +95,25 @@ fn execute(flags: Flags, shell: &mut MultiShell) -> CliResult<Option<()>> {
             let r = cargo::call_main_without_stdin(execute, shell,
                                                    ["-h".to_string()], false);
             cargo::process_executed(r, shell)
-        }
+        },
         orig_cmd => {
-            let cmd = if orig_cmd == "help" {
+            let is_help = orig_cmd == "help";
+            let cmd = if is_help {
                 flags.arg_args[0].as_slice()
             } else {
                 orig_cmd
             };
-            let command = format!("cargo-{}{}", cmd, os::consts::EXE_SUFFIX);
-            let mut command = match os::self_exe_path() {
-                Some(path) => {
-                    let p1 = path.join("../lib/cargo").join(command.as_slice());
-                    let p2 = path.join(command.as_slice());
-                    if p1.exists() {
-                        Command::new(p1)
-                    } else if p2.exists() {
-                        Command::new(p2)
-                    } else {
-                        Command::new(command)
-                    }
-                }
-                None => Command::new(command),
-            };
-            let command = if orig_cmd == "help" {
+            execute_subcommand(cmd, is_help, &flags, shell)
+        }
+    }
+    Ok(None)
+}
+
+fn execute_subcommand(cmd: &str, is_help: bool, flags: &Flags, shell: &mut MultiShell) -> () {
+    match find_command(cmd) {
+        Some(command) => {
+            let mut command = Command::new(command);
+            let command = if is_help {
                 command.arg("-h")
             } else {
                 command.args(flags.arg_args.as_slice())
@@ -124,12 +133,81 @@ fn execute(flags: Flags, shell: &mut MultiShell) -> CliResult<Option<()>> {
                     let msg = format!("subcommand failed with signal: {}", i);
                     handle_error(CliError::new(msg, i as uint), shell)
                 }
-                Err(_) => handle_error(CliError::new("No such subcommand", 127),
-                                       shell)
+                Err(io::IoError{kind, ..}) if kind == io::FileNotFound =>
+                    handle_error(CliError::new("No such subcommand", 127), shell),
+                Err(err) => handle_error(
+                    CliError::new(
+                        format!("Subcommand failed to run: {}", err), 127),
+                    shell)
+            }
+        },
+        None => handle_error(CliError::new("No such subcommand", 127), shell)
+    }
+}
+
+/// List all runnable commands. find_command should always succeed
+/// if given one of returned command.
+fn list_commands() -> TreeSet<String> {
+    let command_prefix = "cargo-";
+    let mut commands = TreeSet::new();
+    for dir in list_command_directory().iter() {
+        let entries = match fs::readdir(dir) {
+            Ok(entries) => entries,
+            _ => continue
+        };
+        for entry in entries.iter() {
+            let filename = match entry.filename_str() {
+                Some(filename) => filename,
+                _ => continue
+            };
+            if filename.starts_with(command_prefix) &&
+                    filename.ends_with(os::consts::EXE_SUFFIX) &&
+                    is_executable(entry) {
+                let command = filename.slice(
+                    command_prefix.len(),
+                    filename.len() - os::consts::EXE_SUFFIX.len());
+                commands.insert(String::from_str(command));
             }
         }
     }
-    Ok(None)
+    commands
+}
+
+fn is_executable(path: &Path) -> bool {
+    match fs::stat(path) {
+        Ok(io::FileStat{kind, perm, ..}) =>
+            (kind == io::TypeFile) && perm.contains(io::OtherExecute),
+        _ => false
+    }
+}
+
+/// Get `Command` to run given command.
+fn find_command(cmd: &str) -> Option<Path> {
+    let command_exe = format!("cargo-{}{}", cmd, os::consts::EXE_SUFFIX);
+    let dirs = list_command_directory();
+    let mut command_paths = dirs.iter().map(|dir| dir.join(command_exe.as_slice()));
+    command_paths.find(|path| path.exists())
+}
+
+/// List candidate locations where subcommands might be installed.
+fn list_command_directory() -> Vec<Path> {
+    let mut dirs = vec![];
+    match os::self_exe_path() {
+        Some(path) => {
+            dirs.push(path.join("../lib/cargo"));
+            dirs.push(path);
+        },
+        None => {}
+    };
+    match std::os::getenv("PATH") {
+        Some(val) => {
+            for dir in os::split_paths(val).iter() {
+                dirs.push(Path::new(dir))
+            }
+        },
+        None => {}
+    };
+    dirs
 }
 
 #[deriving(Encodable)]
diff --git a/tests/test_cargo.rs b/tests/test_cargo.rs
new file mode 100644 (file)
index 0000000..bf32eda
--- /dev/null
@@ -0,0 +1,69 @@
+use cargo::util::{process, ProcessBuilder};
+use hamcrest::{assert_that};
+use std::io;
+use std::io::fs;
+use std::os;
+use support::paths;
+use support::{project, execs, cargo_dir, mkdir_recursive, ProjectBuilder, ResultTest};
+
+fn setup() {
+}
+
+/// Add an empty file with executable flags (and platform-dependent suffix).
+/// TODO: move this to `ProjectBuilder` if other cases using this emerge.
+fn fake_executable(proj: ProjectBuilder, dir: &Path, name: &str) -> ProjectBuilder {
+    let path = proj.root().join(dir).join(format!("{}{}", name, os::consts::EXE_SUFFIX));
+    mkdir_recursive(&Path::new(path.dirname())).assert();
+    fs::File::create(&path).assert();
+    let io::FileStat{perm, ..} = fs::stat(&path).assert();
+    fs::chmod(&path, io::OtherExecute | perm).assert();
+    proj
+}
+
+/// Copy real cargo exeutable just built to specified location, and
+/// prepare to run it.
+fn copied_executable_process(proj: &ProjectBuilder, name: &str, dir: &Path) -> ProcessBuilder {
+    let name = format!("{}{}", name, os::consts::EXE_SUFFIX);
+    let path_src = cargo_dir().join(name.clone());
+    let path_dst = proj.root().join(dir).join(name);
+    mkdir_recursive(&Path::new(path_dst.dirname())).assert();
+    fs::copy(&path_src, &path_dst).assert();
+    process(path_dst)
+        .cwd(proj.root())
+        .env("HOME", Some(paths::home().as_vec()))
+}
+
+test!(list_commands_empty {
+    let proj = project("list-runs");
+    let pr = copied_executable_process(&proj, "cargo", &Path::new("bin")).arg("--list");
+    assert_that(pr, execs()
+        .with_status(0)
+        .with_stdout("Installed Commands:\n"));
+})
+
+test!(list_commands_non_overlapping {
+    // lib/cargo | cargo-3
+    // bin/       | cargo-2
+    // PATH       | cargo-1
+    // Check if --list searches all 3 targets.
+    // Also checks that results are in lexicographic order.
+    let proj = project("list-non-overlapping");
+    let proj = fake_executable(proj, &Path::new("lib/cargo"), "cargo-3");
+    let proj = fake_executable(proj, &Path::new("bin"), "cargo-2");
+    let proj = fake_executable(proj, &Path::new("path-test"), "cargo-1");
+    let pr = copied_executable_process(&proj, "cargo", &Path::new("bin")).arg("--list");
+
+    let path_test = proj.root().join("path-test");
+    // On Windows, cargo.exe seems to require some directory (
+    // I don't know which) to run properly.
+    // That's why we append to $PATH here, instead of overwriting.
+    let path = os::getenv_as_bytes("PATH").unwrap();
+    let mut components = os::split_paths(path);
+    components.push(path_test);
+    let path_var = os::join_paths(components.as_slice()).assert();
+    assert_that(
+        pr.env("PATH", Some(path_var.as_slice())),
+        execs()
+            .with_status(0)
+            .with_stdout("Installed Commands:\n   1\n   2\n   3\n"));
+})
index 3672cd33a3c762390831c52e85ed41f444760816..73ca0dea636afd79228a1f371f5edb59c7e0407a 100644 (file)
@@ -20,6 +20,7 @@ macro_rules! test(
     )
 )
 
+mod test_cargo;
 mod test_cargo_clean;
 mod test_cargo_compile;
 mod test_cargo_compile_git_deps;